/*
 * Copyright (C) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.io;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.List;
import java.util.zip.Checksum;

import com.google.common.base.Preconditions;

/**
 * Provides utility methods for working with files.
 * 
 * <p>
 * All method parameters must be non-null unless documented otherwise.
 * 
 * @author Chris Nokleberg
 * @since 2009.09.15 <b>tentative</b>
 */
public final class Files {

    /** Maximum loop count when creating temp directories. */
    private static final int TEMP_DIR_ATTEMPTS = 10000;

    private Files() {
    }

    /**
     * Returns a buffered reader that reads from a file using the given
     * character set.
     * 
     * @param file
     *            the file to read from
     * @param charset
     *            the character set used when writing the file
     * @return the buffered reader
     */
    public static BufferedReader newReader(File file, Charset charset)
            throws FileNotFoundException {
        return new BufferedReader(new InputStreamReader(new FileInputStream(
                file), charset));
    }

    /**
     * Returns a buffered writer that writes to a file using the given character
     * set.
     * 
     * @param file
     *            the file to write to
     * @param charset
     *            the character set used when writing the file
     * @return the buffered writer
     */
    public static BufferedWriter newWriter(File file, Charset charset)
            throws FileNotFoundException {
        return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(
                file), charset));
    }

    /**
     * Returns a factory that will supply instances of {@link FileInputStream}
     * that read from a file.
     * 
     * @param file
     *            the file to read from
     * @return the factory
     */
    public static InputSupplier<FileInputStream> newInputStreamSupplier(
            final File file) {
        Preconditions.checkNotNull(file);
        return new InputSupplier<FileInputStream>() {
            public FileInputStream getInput() throws IOException {
                return new FileInputStream(file);
            }
        };
    }

    /**
     * Returns a factory that will supply instances of {@link FileOutputStream}
     * that write to a file.
     * 
     * @param file
     *            the file to write to
     * @return the factory
     */
    public static OutputSupplier<FileOutputStream> newOutputStreamSupplier(
            File file) {
        return newOutputStreamSupplier(file, false);
    }

    /**
     * Returns a factory that will supply instances of {@link FileOutputStream}
     * that write to or append to a file.
     * 
     * @param file
     *            the file to write to
     * @param append
     *            if true, the encoded characters will be appended to the file;
     *            otherwise the file is overwritten
     * @return the factory
     */
    public static OutputSupplier<FileOutputStream> newOutputStreamSupplier(
            final File file, final boolean append) {
        Preconditions.checkNotNull(file);
        return new OutputSupplier<FileOutputStream>() {
            public FileOutputStream getOutput() throws IOException {
                return new FileOutputStream(file, append);
            }
        };
    }

    /**
     * Returns a factory that will supply instances of {@link InputStreamReader}
     * that read a file using the given character set.
     * 
     * @param file
     *            the file to read from
     * @param charset
     *            the character set used when reading the file
     * @return the factory
     */
    public static InputSupplier<InputStreamReader> newReaderSupplier(File file,
            Charset charset) {
        return CharStreams.newReaderSupplier(newInputStreamSupplier(file),
                charset);
    }

    /**
     * Returns a factory that will supply instances of
     * {@link OutputStreamWriter} that write to a file using the given character
     * set.
     * 
     * @param file
     *            the file to write to
     * @param charset
     *            the character set used when writing the file
     * @return the factory
     */
    public static OutputSupplier<OutputStreamWriter> newWriterSupplier(
            File file, Charset charset) {
        return newWriterSupplier(file, charset, false);
    }

    /**
     * Returns a factory that will supply instances of
     * {@link OutputStreamWriter} that write to or append to a file using the
     * given character set.
     * 
     * @param file
     *            the file to write to
     * @param charset
     *            the character set used when writing the file
     * @param append
     *            if true, the encoded characters will be appended to the file;
     *            otherwise the file is overwritten
     * @return the factory
     */
    public static OutputSupplier<OutputStreamWriter> newWriterSupplier(
            File file, Charset charset, boolean append) {
        return CharStreams.newWriterSupplier(
                newOutputStreamSupplier(file, append), charset);
    }

    /**
     * Reads all bytes from a file into a byte array.
     * 
     * @param file
     *            the file to read from
     * @return a byte array containing all the bytes from file
     * @throws IllegalArgumentException
     *             if the file is bigger than the largest possible byte array
     *             (2^31 - 1)
     * @throws IOException
     *             if an I/O error occurs
     */
    public static byte[] toByteArray(File file) throws IOException {
        Preconditions.checkArgument(file.length() <= Integer.MAX_VALUE);
        if (file.length() == 0) {
            // Some special files are length 0 but have content nonetheless.
            return ByteStreams.toByteArray(newInputStreamSupplier(file));
        } else {
            // Avoid an extra allocation and copy.
            byte[] b = new byte[(int) file.length()];
            boolean threw = true;
            InputStream in = new FileInputStream(file);
            try {
                ByteStreams.readFully(in, b);
                threw = false;
            } finally {
                Closeables.close(in, threw);
            }
            return b;
        }
    }

    /**
     * Reads all characters from a file into a {@link String}, using the given
     * character set.
     * 
     * @param file
     *            the file to read from
     * @param charset
     *            the character set used when reading the file
     * @return a string containing all the characters from the file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static String toString(File file, Charset charset)
            throws IOException {
        return new String(toByteArray(file), charset.name());
    }

    /**
     * Copies to a file all bytes from an {@link InputStream} supplied by a
     * factory.
     * 
     * @param from
     *            the input factory
     * @param to
     *            the destination file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void copy(InputSupplier<? extends InputStream> from, File to)
            throws IOException {
        ByteStreams.copy(from, newOutputStreamSupplier(to));
    }

    /**
     * Overwrites a file with the contents of a byte array.
     * 
     * @param from
     *            the bytes to write
     * @param to
     *            the destination file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void write(byte[] from, File to) throws IOException {
        ByteStreams.write(from, newOutputStreamSupplier(to));
    }

    /**
     * Copies all bytes from a file to an {@link OutputStream} supplied by a
     * factory.
     * 
     * @param from
     *            the source file
     * @param to
     *            the output factory
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void copy(File from, OutputSupplier<? extends OutputStream> to)
            throws IOException {
        ByteStreams.copy(newInputStreamSupplier(from), to);
    }

    /**
     * Copies all bytes from a file to an output stream.
     * 
     * @param from
     *            the source file
     * @param to
     *            the output stream
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void copy(File from, OutputStream to) throws IOException {
        ByteStreams.copy(newInputStreamSupplier(from), to);
    }

    /**
     * Copies all the bytes from one file to another. .
     * 
     * @param from
     *            the source file
     * @param to
     *            the destination file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void copy(File from, File to) throws IOException {
        copy(newInputStreamSupplier(from), to);
    }

    /**
     * Copies to a file all characters from a {@link Readable} and
     * {@link Closeable} object supplied by a factory, using the given character
     * set.
     * 
     * @param from
     *            the readable supplier
     * @param to
     *            the destination file
     * @param charset
     *            the character set used when writing the file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static <R extends Readable & Closeable> void copy(
            InputSupplier<R> from, File to, Charset charset) throws IOException {
        CharStreams.copy(from, newWriterSupplier(to, charset));
    }

    /**
     * Writes a character sequence (such as a string) to a file using the given
     * character set.
     * 
     * @param from
     *            the character sequence to write
     * @param to
     *            the destination file
     * @param charset
     *            the character set used when writing the file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void write(CharSequence from, File to, Charset charset)
            throws IOException {
        write(from, to, charset, false);
    }

    /**
     * Appends a character sequence (such as a string) to a file using the given
     * character set.
     * 
     * @param from
     *            the character sequence to append
     * @param to
     *            the destination file
     * @param charset
     *            the character set used when writing the file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void append(CharSequence from, File to, Charset charset)
            throws IOException {
        write(from, to, charset, true);
    }

    /**
     * Private helper method. Writes a character sequence to a file, optionally
     * appending.
     * 
     * @param from
     *            the character sequence to append
     * @param to
     *            the destination file
     * @param charset
     *            the character set used when writing the file
     * @param append
     *            true to append, false to overwrite
     * @throws IOException
     *             if an I/O error occurs
     */
    private static void write(CharSequence from, File to, Charset charset,
            boolean append) throws IOException {
        CharStreams.write(from, newWriterSupplier(to, charset, append));
    }

    /**
     * Copies all characters from a file to a {@link Appendable} &
     * {@link Closeable} object supplied by a factory, using the given character
     * set.
     * 
     * @param from
     *            the source file
     * @param charset
     *            the character set used when reading the file
     * @param to
     *            the appendable supplier
     * @throws IOException
     *             if an I/O error occurs
     */
    public static <W extends Appendable & Closeable> void copy(File from,
            Charset charset, OutputSupplier<W> to) throws IOException {
        CharStreams.copy(newReaderSupplier(from, charset), to);
    }

    /**
     * Copies all characters from a file to an appendable object, using the
     * given character set.
     * 
     * @param from
     *            the source file
     * @param charset
     *            the character set used when reading the file
     * @param to
     *            the appendable object
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void copy(File from, Charset charset, Appendable to)
            throws IOException {
        CharStreams.copy(newReaderSupplier(from, charset), to);
    }

    /**
     * Returns true if the files contains the same bytes.
     * 
     * @throws IOException
     *             if an I/O error occurs
     */
    public static boolean equal(File file1, File file2) throws IOException {
        if (file1 == file2 || file1.equals(file2)) {
            return true;
        }

        /*
         * Some operating systems may return zero as the length for files
         * denoting system-dependent entities such as devices or pipes, in which
         * case we must fall back on comparing the bytes directly.
         */
        long len1 = file1.length();
        long len2 = file2.length();
        if (len1 != 0 && len2 != 0 && len1 != len2) {
            return false;
        }
        return ByteStreams.equal(newInputStreamSupplier(file1),
                newInputStreamSupplier(file2));
    }

    /**
     * Atomically creates a new directory somewhere beneath the system's
     * temporary directory (as defined by the {@code java.io.tmpdir} system
     * property), and returns its name.
     * 
     * <p>
     * Use this method instead of {@link File#createTempFile(String, String)}
     * when you wish to create a directory, not a regular file. A common pitfall
     * is to call {@code createTempFile}, delete the file and create a directory
     * in its place, but this leads a race condition which can be exploited to
     * create security vulnerabilities, especially when executable files are to
     * be written into the directory.
     * 
     * <p>
     * This method assumes that the temporary volume is writable, has free
     * inodes and free blocks, and that it will not be called thousands of times
     * per second.
     * 
     * @return the newly-created directory
     * @throws IllegalStateException
     *             if the directory could not be created
     */
    public static File createTempDir() {
        File baseDir = new File(System.getProperty("java.io.tmpdir"));
        String baseName = System.currentTimeMillis() + "-";

        for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
            File tempDir = new File(baseDir, baseName + counter);
            if (tempDir.mkdir()) {
                return tempDir;
            }
        }
        throw new IllegalStateException("Failed to create directory within "
                + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to "
                + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
    }

    /**
     * Creates an empty file or updates the last updated timestamp on the same
     * as the unix command of the same name.
     * 
     * @param file
     *            the file to create or update
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void touch(File file) throws IOException {
        if (!file.createNewFile()
                && !file.setLastModified(System.currentTimeMillis())) {
            throw new IOException("Unable to update modification time of "
                    + file);
        }
    }

    /**
     * Moves the file from one path to another. This method can rename a file or
     * move it to a different directory, like the Unix {@code mv} command.
     * 
     * @param from
     *            the source file
     * @param to
     *            the destination file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void move(File from, File to) throws IOException {
        Preconditions.checkNotNull(to);
        Preconditions.checkArgument(!from.equals(to),
                "Source %s and destination %s must be different", from, to);

        if (!from.renameTo(to)) {
            copy(from, to);
            if (!from.delete()) {
                if (!to.delete()) {
                    throw new IOException("Unable to delete " + to);
                }
                throw new IOException("Unable to delete " + from);
            }
        }
    }

    /**
     * Deletes all the files within a directory. Does not delete the directory
     * itself.
     * 
     * <p>
     * If the file argument is a symbolic link or there is a symbolic link in
     * the path leading to the directory, this method will do nothing. Symbolic
     * links within the directory are not followed.
     * 
     * @param directory
     *            the directory to delete the contents of
     * @throws IllegalArgumentException
     *             if the argument is not a directory
     * @throws IOException
     *             if an I/O error occurs
     * @see #deleteRecursively
     */
    public static void deleteDirectoryContents(File directory)
            throws IOException {
        Preconditions.checkArgument(directory.isDirectory(),
                "Not a directory: %s", directory);
        // Symbolic links will have different canonical and absolute paths
        if (!directory.getCanonicalPath().equals(directory.getAbsolutePath())) {
            return;
        }
        File[] files = directory.listFiles();
        if (files == null) {
            throw new IOException("Error listing files for " + directory);
        }
        for (File file : files) {
            deleteRecursively(file);
        }
    }

    /**
     * Deletes a file or directory and all contents recursively.
     * 
     * <p>
     * If the file argument is a symbolic link the link will be deleted but not
     * the target of the link. If the argument is a directory, symbolic links
     * within the directory will not be followed.
     * 
     * @param file
     *            the file to delete
     * @throws IOException
     *             if an I/O error occurs
     * @see #deleteDirectoryContents
     */
    public static void deleteRecursively(File file) throws IOException {
        if (file.isDirectory()) {
            deleteDirectoryContents(file);
        }
        if (!file.delete()) {
            throw new IOException("Failed to delete " + file);
        }
    }

    /**
     * Reads the first line from a file. The line does not include
     * line-termination characters, but does include other leading and trailing
     * whitespace.
     * 
     * @param file
     *            the file to read from
     * @param charset
     *            the character set used when writing the file
     * @return the first line, or null if the file is empty
     * @throws IOException
     *             if an I/O error occurs
     */
    public static String readFirstLine(File file, Charset charset)
            throws IOException {
        return CharStreams
                .readFirstLine(Files.newReaderSupplier(file, charset));
    }

    /**
     * Reads all of the lines from a file. The lines do not include
     * line-termination characters, but do include other leading and trailing
     * whitespace.
     * 
     * @param file
     *            the file to read from
     * @param charset
     *            the character set used when writing the file
     * @return a mutable {@link List} containing all the lines
     * @throws IOException
     *             if an I/O error occurs
     */
    public static List<String> readLines(File file, Charset charset)
            throws IOException {
        return CharStreams.readLines(Files.newReaderSupplier(file, charset));
    }

    /**
     * Streams lines from a {@link File}, stopping when our callback returns
     * false, or we have read all of the lines.
     * 
     * @param file
     *            the file to read from
     * @param charset
     *            the character set used when writing the file
     * @param callback
     *            the {@link LineProcessor} to use to handle the lines
     * @return the output of processing the lines
     * @throws IOException
     *             if an I/O error occurs
     */
    public static <T> T readLines(File file, Charset charset,
            LineProcessor<T> callback) throws IOException {
        return CharStreams.readLines(Files.newReaderSupplier(file, charset),
                callback);
    }

    /**
     * Process the bytes of a file.
     * 
     * <p>
     * (If this seems too complicated, maybe you're looking for
     * {@link #toByteArray}.)
     * 
     * @param file
     *            the file to read
     * @param processor
     *            the object to which the bytes of the file are passed.
     * @return the result of the byte processor
     * @throws IOException
     *             if an I/O error occurs
     */
    public static <T> T readBytes(File file, ByteProcessor<T> processor)
            throws IOException {
        return ByteStreams.readBytes(newInputStreamSupplier(file), processor);
    }

    /**
     * Computes and returns the checksum value for a file. The checksum object
     * is reset when this method returns successfully.
     * 
     * @param file
     *            the file to read
     * @param checksum
     *            the checksum object
     * @return the result of {@link Checksum#getValue} after updating the
     *         checksum object with all of the bytes in the file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static long getChecksum(File file, Checksum checksum)
            throws IOException {
        return ByteStreams.getChecksum(newInputStreamSupplier(file), checksum);
    }

    /**
     * Computes and returns the digest value for a file. The digest object is
     * reset when this method returns successfully.
     * 
     * @param file
     *            the file to read
     * @param md
     *            the digest object
     * @return the result of {@link MessageDigest#digest()} after updating the
     *         digest object with all of the bytes in this file
     * @throws IOException
     *             if an I/O error occurs
     */
    public static byte[] getDigest(File file, MessageDigest md)
            throws IOException {
        return ByteStreams.getDigest(newInputStreamSupplier(file), md);
    }

    /**
     * Fully maps a file read-only in to memory as per
     * {@link FileChannel#map(java.nio.channels.FileChannel.MapMode, long, long)}
     * .
     * 
     * <p>
     * Files are mapped from offset 0 to its length.
     * 
     * <p>
     * This only works for files <= {@link Integer#MAX_VALUE} bytes.
     * 
     * @param file
     *            the file to map
     * @return a read-only buffer reflecting {@code file}
     * @throws FileNotFoundException
     *             if the {@code file} does not exist
     * @throws IOException
     *             if an I/O error occurs
     * 
     * @see FileChannel#map(MapMode, long, long)
     * @since 2010.01.04 <b>tentative</b>
     */
    public static MappedByteBuffer map(File file) throws IOException {
        return map(file, MapMode.READ_ONLY);
    }

    /**
     * Fully maps a file in to memory as per
     * {@link FileChannel#map(java.nio.channels.FileChannel.MapMode, long, long)}
     * using the requested {@link MapMode}.
     * 
     * <p>
     * Files are mapped from offset 0 to its length.
     * 
     * <p>
     * This only works for files <= {@link Integer#MAX_VALUE} bytes.
     * 
     * @param file
     *            the file to map
     * @param mode
     *            the mode to use when mapping {@code file}
     * @return a buffer reflecting {@code file}
     * @throws FileNotFoundException
     *             if the {@code file} does not exist
     * @throws IOException
     *             if an I/O error occurs
     * 
     * @see FileChannel#map(MapMode, long, long)
     * @since 2010.01.04 <b>tentative</b>
     */
    public static MappedByteBuffer map(File file, MapMode mode)
            throws IOException {
        if (!file.exists()) {
            throw new FileNotFoundException(file.toString());
        }
        return map(file, mode, file.length());
    }

    /**
     * Maps a file in to memory as per
     * {@link FileChannel#map(java.nio.channels.FileChannel.MapMode, long, long)}
     * using the requested {@link MapMode}.
     * 
     * <p>
     * Files are mapped from offset 0 to {@code size}.
     * 
     * <p>
     * If the mode is {@link MapMode#READ_WRITE} and the file does not exist, it
     * will be created with the requested {@code size}. Thus this method is
     * useful for creating memory mapped files which do not yet exist.
     * 
     * <p>
     * This only works for files <= {@link Integer#MAX_VALUE} bytes.
     * 
     * @param file
     *            the file to map
     * @param mode
     *            the mode to use when mapping {@code file}
     * @return a buffer reflecting {@code file}
     * @throws IOException
     *             if an I/O error occurs
     * 
     * @see FileChannel#map(MapMode, long, long)
     * @since 2010.01.04 <b>tentative</b>
     */
    public static MappedByteBuffer map(File file, MapMode mode, long size)
            throws FileNotFoundException, IOException {
        RandomAccessFile raf = new RandomAccessFile(file,
                mode == MapMode.READ_ONLY ? "r" : "rw");

        boolean threw = true;
        try {
            MappedByteBuffer mbb = map(raf, mode, size);
            threw = false;
            return mbb;
        } finally {
            Closeables.close(raf, threw);
        }
    }

    private static MappedByteBuffer map(RandomAccessFile raf, MapMode mode,
            long size) throws IOException {
        FileChannel channel = raf.getChannel();

        boolean threw = true;
        try {
            MappedByteBuffer mbb = channel.map(mode, 0, size);
            threw = false;
            return mbb;
        } finally {
            Closeables.close(channel, threw);
        }
    }
}
